import * as t from '@babel/types'
import { createHmac } from 'crypto'
import Debug from 'debug'
import { isFunction, isNative } from 'lodash-es'
import { posix as posixPath } from 'path'
import babelGenerator from '@babel/generator'

export function getIgnore(exclude: string[] = []) {
  return ['node_modules', '.git', '**/__*__/**', ...exclude]
}

const _types$1 = ['string', 'symbol', 'function', 'number', 'boolean']
/**
 * Convert `Arrayable<T>` to `Array<T>`
 *
 * @category Array
 */
export function toArray<T>(maybeArray?: any): Array<T> {
  if (Array.isArray(maybeArray)) return maybeArray
  if (_types$1.includes(typeof maybeArray)) return [maybeArray]
  if (
    typeof maybeArray?.['length'] === 'number' ||
    typeof maybeArray?.[Symbol.iterator] === 'function'
  ) {
    return Array.from(maybeArray)
  } else if (maybeArray === undefined) return []
  else return [maybeArray]
}

export function extensionsToGlob(extensions: string[]) {
  return extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0] ?? ''
}

export function normalizePath(path: string) {
  return posixPath.normalize(path.replace(/\\/g, posixPath.sep))
}

export const debug = {
  hmr: Debug('vite-plugin-route-pages:hmr'),
  parser: Debug('vite-plugin-route-pages:parser'),
  gen: Debug('vite-plugin-route-pages:gen'),
  options: Debug('vite-plugin-route-pages:options'),
  cache: Debug('vite-plugin-route-pages:cache'),
  pages: Debug('vite-plugin-route-pages:pages'),
}

export type UnkownValue = typeof UnkownValue
export const UnkownValue = Symbol.for('UnkownValue')

export class AnyValueWrapper {
  constructor(public value: string) {}
}

export function parseConstance(node: t.Node, constances?: Record<string, any>): any {
  try {
    if (node.type === 'UnaryExpression') {
      const { operator, argument } = node
      if (operator === 'void') return undefined
      const argumentValue = parseConstance(argument, constances)
      if (argumentValue !== UnkownValue) {
        switch (operator) {
          case '!':
            return !argumentValue
          case '+':
            return +argumentValue
          case '-':
            return -argumentValue
          case 'typeof':
            return typeof argumentValue
        }
      }
    } else if (node.type === 'BinaryExpression') {
      const { operator, left, right } = node
      const leftValue = parseConstance(left, constances)
      const rightValue = parseConstance(right, constances)
      if (leftValue !== UnkownValue && rightValue !== UnkownValue) {
        switch (operator) {
          case '!=':
            return leftValue != rightValue
          case '!==':
            return leftValue !== rightValue
          case '%':
            return leftValue % rightValue
          case '&':
            return leftValue & rightValue
          case '*':
            return leftValue * rightValue
          case '**':
            return leftValue ** rightValue
          case '+':
            return leftValue + rightValue
          case '-':
            return leftValue - rightValue
          case '/':
            return leftValue / rightValue
          case '<':
            return leftValue < rightValue
          case '<<':
            return leftValue << rightValue
          case '<=':
            return leftValue <= rightValue
          case '==':
            return leftValue == rightValue
          case '===':
            return leftValue === rightValue
          case '>':
            return leftValue > rightValue
          case '>=':
            return leftValue >= rightValue
          case '>>':
            return leftValue >> rightValue
          case '>>>':
            return leftValue >>> rightValue
          case '^':
            return leftValue ^ rightValue
          case '|':
            return leftValue | rightValue
        }
      }
    } else if (node.type === 'TemplateLiteral') {
      const quasis = node.quasis.map((node) => node.value.cooked ?? '')
      const expressions = node.expressions.map((node) => parseConstance(node, constances))
      if (expressions.every((val) => val !== UnkownValue)) {
        let str = quasis.shift()
        for (let i = 0, len = expressions.length; i < len; ++i) {
          str += expressions[i]
          str += quasis[0]
        }
      }
    } else if (node.type === 'Identifier') {
      const { name } = node
      if (name === 'undefined') {
        return 'undefined'
      }
      if (constances && name in constances) {
        return constances[name]
      }
      return UnkownValue
    } else if (t.isFunction(node)) {
      if ((node as any).declare) return UnkownValue
      let func: t.Function
      try {
        if (t.isArrowFunctionExpression(node)) {
          func = node
        } else {
          let { body, generator, async, params } = node
          let id: t.Identifier | undefined = (node as any).id
          params = params.map((param) => {
            return param.type === 'TSParameterProperty' ? param.parameter : param
          })
          func = t.functionExpression(id, params as any, body, generator, async)
          return new AnyValueWrapper(babelGenerator(func).code)
        }
      } catch {}
      return UnkownValue
    } else if (t.isJSXElement(node)) {
      return new AnyValueWrapper(babelGenerator(node).code)
    } else if (t.isClass(node)) {
      return new AnyValueWrapper(babelGenerator(node).code)
    } else {
      switch (node.type) {
        case 'TSAsExpression':
        case 'TSNonNullExpression':
          return parseConstance(node.expression, constances)
        case 'SequenceExpression':
          return parseConstance(node.expressions[node.expressions.length - 1], constances)
        case 'AssignmentExpression':
          return node.operator === '=' ? parseConstance(node.right, constances) : UnkownValue
        case 'StringLiteral':
        case 'DecimalLiteral':
        case 'BigIntLiteral':
        case 'NumericLiteral':
        case 'BooleanLiteral':
          return node.value
        case 'NullLiteral':
          return null
        case 'ObjectExpression':
          return parseObject(node, constances)
        case 'ArrayExpression':
          return parseArray(node, constances)
      }
    }
  } catch {}
  return UnkownValue
}

function parseObject(node: t.ObjectExpression, constances?: Record<string, any>) {
  let obj: Record<keyof any, any> = {}
  for (const prop of node.properties) {
    if (prop.type === 'SpreadElement') {
      const value = parseConstance(prop.argument, constances)
      if (value === UnkownValue) obj = {}
      else Object.assign(obj, value)
    } else if (prop.type === 'ObjectMethod') {
      const { key, computed } = prop
      const keyValue = computed
        ? parseConstance(key, constances)
        : key.type === 'Identifier'
        ? key.name
        : UnkownValue
      if (keyValue === UnkownValue) obj = {}
      else delete obj[keyValue]
    } else {
      const { key, computed } = prop
      const keyValue = computed
        ? parseConstance(key, constances)
        : key.type === 'Identifier'
        ? key.name
        : UnkownValue
      if (keyValue === UnkownValue) obj = {}
      else obj[keyValue] = parseConstance(prop.value, constances)
    }
  }

  return obj
}

function parseArray(node: t.ArrayExpression, constances?: Record<string, any>) {
  let array: any[] = []
  for (const elem of node.elements) {
    if (!elem) {
      array.length++
    } else if (elem.type === 'SpreadElement') {
      const spread = parseConstance(elem, constances)
      try {
        array.push(...spread)
      } catch (error) {
        break
      }
    } else {
      array.push(parseConstance(elem, constances))
    }
  }

  return array
}

const secret = '4h54h4hru'

export function hashString(source: string, prefix = '') {
  return prefix + createHmac('sha256', secret).update(source).digest('base64url').replace(/-/g, '$')
}

let i = 0
const getUniqueString = () => {
  return (
    createHmac('sha256', String(Math.random())).update(String(Date.now())).digest('base64') + ++i
  )
}

export const createAnyValueStringify = (
  stringifyValue?: (this: any, key: string, value: any) => string | void
) => {
  const replacements = new Map<string, string>()

  const addPlaceholderAndReturn = (value: string) => {
    let uniStr = getUniqueString()
    replacements.set(`"${uniStr}"`, value)
    return uniStr
  }

  function replacer(this: any, key: string, value: any) {
    const generateVal = stringifyValue?.call(this, key, value)
    if (generateVal != null) {
      return addPlaceholderAndReturn(generateVal)
    } else if (value instanceof Date) {
      return addPlaceholderAndReturn(`new Date(${value.getTime()})`)
    } else if (value instanceof RegExp) {
      return addPlaceholderAndReturn(`/${value.source}/${value.flags}`)
    } else if (value instanceof AnyValueWrapper) {
      return addPlaceholderAndReturn(value.value)
    } else if (value instanceof Map) {
      return addPlaceholderAndReturn(`new Map(${JSON.stringify(Array.from(value), replacer)})`)
    } else if (value instanceof Set) {
      return addPlaceholderAndReturn(`new Set(${JSON.stringify(Array.from(value), replacer)})`)
    } else if (isFunction(value)) {
      if (isNative(value)) {
        const nativeFuncStr = nativeFuncMap.get(value)
        if (nativeFuncStr) {
          return addPlaceholderAndReturn(nativeFuncStr)
        }
      } else {
        return addPlaceholderAndReturn(Function.prototype.toString.call(value))
      }
    }
    return value
  }

  return (value: any) => {
    let str = Array.isArray(value)
      ? `[${value.map((value) => JSON.stringify(value, replacer)).join(',')}]`
      : JSON.stringify(value, replacer)
    if (str) {
      for (const [placeholder, value] of replacements) {
        str = str.replace(placeholder, value)
      }
    }
    return str
  }
}

const nativeFuncMap = new Map<Function, string>(
  Object.entries({
    Object,
    Array,
    String,
    Boolean,
    Function,
    Symbol,
    console,
    Reflect,
    Date,
    Number,
    Math,
    parseInt,
    parseFloat,
    Error,
    Set,
    Map,
    WeakMap,
    WeakSet,
    Buffer,
    Int8Array,
    Int16Array,
    Int32Array,
    Uint8Array,
    Uint8ClampedArray,
    Uint16Array,
    Uint32Array,
    BigInt64Array,
    BigUint64Array,
    Float32Array,
    Float64Array,
  }).reduce((array, [key, value]) => {
    if (typeof value === 'function') {
      array.push([value, key])
      if (value.prototype && value !== Symbol) {
        const _prototype = value.prototype
        Reflect.ownKeys(_prototype).forEach((key$1) => {
          try {
            if (typeof key$1 === 'string') {
              const func = (_prototype as any)[key$1]
              if (typeof func === 'function') {
                array.push([func, `${key}.prototype.${key$1}`])
              }
            }
          } catch {}
        })
      }
    }
    Reflect.ownKeys(value).forEach((key$1) => {
      try {
        if (key$1 === 'prototype') return
        if (typeof key$1 === 'string') {
          const func = (value as any)[key$1]
          if (typeof func === 'function') {
            array.push([func, `${key}.${key$1}`])
          }
        }
      } catch {}
    })
    return array
  }, [] as [Function, string][])
)
